<!DOCTYPE html>
<head>
	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	<title>Portal!</title>
	<script src='js/keyboard.js'></script>
	<!-- include three.js library -->
	<script src='js/three.js'></script>
	<script src='js/stats.min.js'></script>
</head>

<body style='margin : 0px; overflow: hidden; font-family: Monospace;'>

<!-- 
  Example created by Lee Stemkoski: https://github.com/stemkoski
  
  For more information about portal-style effects, read:
      https://en.wikibooks.org/wiki/OpenGL_Programming/Mini-Portal
      https://th0mas.nl/2013/05/19/rendering-recursive-portals-with-opengl/
-->

<script>

var scene, camera, renderer, clock, deltaTime, totalTime, keyboard, stats;

var mainMover, otherMover;
var mainCamera, otherCamera, topCamera;
var portalA, portalB;
var blocker1, blocker2, blocker3;
var portalRing;

initialize();
animate();

function initialize()
{
	scene = new THREE.Scene();
				
	mainCamera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 );
	otherCamera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 );
	topCamera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 1000 );
	
	topCamera.position.set(0, 30, 8);
	topCamera.lookAt( scene.position );	
	scene.add( topCamera );
	
	topCamera.layers.enable(0); // automatic, but added for clarity
	topCamera.layers.enable(1);
	topCamera.layers.enable(2);

	let ambientLight = new THREE.AmbientLight( 0xcccccc, 1.00 );
	scene.add( ambientLight );
	
	// let pointLight = new THREE.PointLight();
	// camera.add( pointLight );
	
	renderer = new THREE.WebGLRenderer({
		antialias : true,
		alpha: false
	});
	renderer.setClearColor(new THREE.Color('lightgrey'), 0)
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.domElement.style.position = 'absolute'
	renderer.domElement.style.top  = '0px'
	renderer.domElement.style.left = '0px'
	document.body.appendChild( renderer.domElement );
	window.addEventListener( 'resize', onWindowResize, false );
	
	stats = new Stats();
	document.body.appendChild( stats.dom );

	clock = new THREE.Clock();
	deltaTime = 0;
	totalTime = 0;
	
	keyboard = new Keyboard();
	
	let loader = new THREE.TextureLoader();
	
	// floor
	let floorGeometry = new THREE.PlaneGeometry(100, 100);
	let floorMaterial = new THREE.MeshBasicMaterial({ 
		map: loader.load( 'images/color-grid.png' ),
		transparent: true,
		opacity: 0.5,
	});
	let floorMesh = new THREE.Mesh( floorGeometry, floorMaterial );
	floorMesh.rotation.x = -Math.PI/2;
	// scene.add( floorMesh );
	
	// material for portals and blockers
	let defaultMaterial = new THREE.MeshBasicMaterial({
		map: loader.load("images/sphere-colored.png"), 
		color: 0x444444,
		side: THREE.DoubleSide,
		transparent: true
	});
	
	// Portal A ================================
	
	// textures from http://www.humus.name/
	let skyMaterialArray1 = [
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/posx.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/negx.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/posy.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/negy.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/posz.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/beach/negz.jpg"), side: THREE.BackSide } ),
	];
	let skyMesh1 = new THREE.Mesh(
		new THREE.CubeGeometry(30,30,30),
		skyMaterialArray1 );
	skyMesh1.position.x = -20;
	scene.add(skyMesh1);
	
	portalA = new THREE.Mesh(
		new THREE.CircleGeometry(1, 64),
		defaultMaterial.clone()
	);
	portalA.material.opacity = 0.5;
	portalA.position.set(-22, 0.5, -3);
	portalA.rotation.y = Math.PI/4;
	portalA.layers.set(1);
	scene.add(portalA);
	
	portalRing = new THREE.Mesh(
		new THREE.RingGeometry(1, 1.1, 64),
		new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide, transparent:true })
	);
	portalRing.position.copy( portalA.position );
	portalRing.rotation.copy( portalA.rotation );
	portalRing.layers.set(0);
	scene.add(portalRing);
	
	// used to visualize position of camera from top view
	let mainCameraMesh = new THREE.Mesh( 
		new THREE.BoxGeometry(1,1,1), 
		new THREE.MeshBasicMaterial({
			map: loader.load("images/border.png"),
			color: 0xff00ff
		}) 
	);

	// used to move main camera around
	mainMover = new THREE.Group();
	mainMover.position.set(-21, 0.5, 0);
	mainMover.add( mainCamera );
	mainMover.add( mainCameraMesh );
	scene.add( mainMover );
	
	// blockers used to check depth test and clipping plane
	
	blocker1 = new THREE.Mesh(
		new THREE.SphereGeometry(0.25, 32, 32),
		defaultMaterial.clone()
	);
	blocker1.material.color = new THREE.Color(0xff0000);
	blocker1.position.set(-21.25, 0.5, -2);
	scene.add(blocker1);
		
	// Portal B ================================
	
	// textures from http://www.humus.name/
	let skyMaterialArray2 = [
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/posx.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/negx.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/posy.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/negy.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/posz.jpg"), side: THREE.BackSide } ),
		new THREE.MeshBasicMaterial( { map: loader.load("images/mountain/negz.jpg"), side: THREE.BackSide } ),
	];
	let skyMesh2 = new THREE.Mesh(
		new THREE.CubeGeometry(30,30,30),
		skyMaterialArray2 );
	skyMesh2.position.x = 20;
	scene.add(skyMesh2);
	
	portalB = new THREE.Mesh(
		new THREE.CircleGeometry(1, 64),
		defaultMaterial.clone()
	);
	portalB.material.opacity = 0.5;
	portalB.position.set(24, 0.5, -5);
	portalB.rotation.y = -Math.PI/4;
	portalB.layers.set(2);
	scene.add(portalB);
	
	// used to visualize position of camera from top view
	let otherCameraMesh = new THREE.Mesh( 
		new THREE.BoxGeometry(1,1,1), 
		new THREE.MeshBasicMaterial({
			map: loader.load("images/border.png"),
			color: 0x00ffff
		}) 
	);

	// used to move other camera around
	otherMover = new THREE.Group();
	otherMover.add( otherCameraMesh );
	otherMover.add( otherCamera );
	scene.add(otherMover);
	
	// blockers used to check depth test and clipping plane
	
	blocker2 = new THREE.Mesh(
		new THREE.SphereGeometry(0.20, 32, 32),
		defaultMaterial.clone()
	);
	blocker2.material.color = new THREE.Color(0x00ff00);
	blocker2.position.set(23.25,0.25,-4.5);
	scene.add(blocker2);
	
	blocker3 = new THREE.Mesh(
		new THREE.SphereGeometry(0.20, 32, 32),
		defaultMaterial.clone()
	);
	blocker3.material.color = new THREE.Color(0x0000ff);
	blocker3.position.set(26,0.25,-6);
	scene.add(blocker3);
}

function update()
{
	stats.update();
	keyboard.update();
	blocker1.rotation.y += 0.01;
	blocker2.rotation.y += 0.01;
	blocker3.rotation.y += 0.01;
	
	// portal ring color cycle
	
	portalRing.material.color.setHSL( totalTime/10 % 1, 1, 0.75 );
	
	// move main camera
	
	let translateSpeed = 0.5; // units per second
	let distance = translateSpeed * deltaTime;
	let rotateSpeed = Math.PI/3; // radians per second
	let angle = rotateSpeed * deltaTime;
	
	if (keyboard.isKeyPressed("W"))
		mainMover.translateZ( -distance );
	if (keyboard.isKeyPressed("S"))
		mainMover.translateZ( distance );
		
	if (keyboard.isKeyPressed("A"))
		mainMover.translateX( -distance );
	if (keyboard.isKeyPressed("D"))
		mainMover.translateX( distance );
		
	if (keyboard.isKeyPressed("R"))
		mainMover.translateY( distance );
	if (keyboard.isKeyPressed("F"))
		mainMover.translateY( -distance );
		
	if (keyboard.isKeyPressed("Q"))
		mainMover.rotateY( angle );
	if (keyboard.isKeyPressed("E"))
		mainMover.rotateY( -angle );
		
	if (keyboard.isKeyPressed("T"))
		mainMover.children[0].rotateX( angle );
	if (keyboard.isKeyPressed("G"))
		mainMover.children[0].rotateX( -angle );
	
	// relatively align other camera with main camera
	
	let relativePosition = portalA.worldToLocal( mainMover.position.clone() );
	otherMover.position.copy( portalB.localToWorld( relativePosition ) );
	
	let relativeRotation = mainMover.quaternion.clone().multiply( portalA.quaternion.clone().inverse() );	
	otherMover.quaternion.copy( relativeRotation.multiply(portalB.quaternion) ); 
	
	// keep camera tilt in sync
	otherCamera.rotation.x = mainCamera.rotation.x;
}

function render()
{
	if ( keyboard.isKeyPressed("1") ) 
	{
		// view from main camera
		mainCamera.layers.enable(0);
		mainCamera.layers.enable(1);
		renderer.render( scene, mainCamera );
	}
	else if ( keyboard.isKeyPressed("2") )
	{
		// view from other camera 
		otherCamera.layers.enable(0);
		otherCamera.layers.enable(2);
		renderer.render( scene, otherCamera );
	}
	else if (keyboard.isKeyPressed("3"))
	{
		// view from top camera
		renderer.render( scene, topCamera );
	}
	else // default: portal effect
	{
		let gl = renderer.context;
		
		
		// clear buffers now: color, depth, stencil 
		renderer.clear(true,true,true);
		// do not clear buffers before each render pass
		renderer.autoClear = false;
		
		
		// FIRST PASS
		// goal: using the stencil buffer, place 1's in position of first portal

		// enable the stencil buffer
		gl.enable(gl.STENCIL_TEST);
		
		// layer 1 contains only the first portal
		mainCamera.layers.set(1); 

		gl.stencilFunc(gl.ALWAYS, 1, 0xff);
		gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
		gl.stencilMask(0xff);

		// only write to stencil buffer (not color or depth)
		gl.colorMask(false,false,false,false);
		gl.depthMask(false);
		
		renderer.render( scene, mainCamera );
		
		// SECOND PASS
		// goal: draw from the portal camera perspective (which is aligned relative to the second portal)
		//   in the first portal region (set by the stencil in the previous pass)
		
		// set up a clipping plane, so that portal camera does not see anything between
		//   the portal camera and the second portal
		
		// default normal of a plane is 0,0,1. apply mesh rotation to it.
	
		
		// determine which side of the plane camera is on, for clipping plane orientation.
		let portalToCamera = new THREE.Vector3().subVectors( mainMover.position.clone(), portalA.position.clone() ); //  applyQuaternion( mainMover.quaternion );
		let normalPortal = new THREE.Vector3(0,0,1).applyQuaternion( portalA.quaternion );
		let clipSide = -Math.sign( portalToCamera.dot(normalPortal) );
		
		let clipNormal = new THREE.Vector3(0, 0, clipSide).applyQuaternion( portalB.quaternion );
		let clipPoint = portalB.position;
		let clipPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(clipNormal, clipPoint);
		renderer.clippingPlanes = [clipPlane];
		
		gl.colorMask(true,true,true,true);
		gl.depthMask(true);
		
		gl.stencilFunc(gl.EQUAL, 1, 0xff);
		gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
		
		otherCamera.layers.set(0);
		renderer.render( scene, otherCamera );
		
		// disable clipping planes
		renderer.clippingPlanes = [];
		
		// THIRD PASS
		// goal: set the depth buffer data for the first portal,
		//   so that it can be occluded by other objects
		
		// finished with stencil
		gl.disable(gl.STENCIL_TEST);
		
		gl.colorMask(false,false,false,false);
		gl.depthMask(true);
		// need to clear the depth buffer, in case of occlusion
		renderer.clear(false, true, false);
		renderer.render( scene, mainCamera );
		
		// FINAL PASS
		// goal: draw the rest of the scene

		gl.colorMask(true,true,true,true);
		gl.depthMask(true);
		
		mainCamera.layers.set(0); // layer 0 contains everything but portals
		renderer.render( scene, mainCamera );
		
		// set things back to normal
		renderer.autoClear = true;
	}
}

function animate()
{
	requestAnimationFrame(animate);
	deltaTime = clock.getDelta();
	totalTime += deltaTime;
	update();
	render();
}

function onWindowResize() 
{
	// camera.aspect = window.innerWidth / window.innerHeight;
	// camera.updateProjectionMatrix();
	renderer.setSize( window.innerWidth, window.innerHeight );
}

</script>

</body>
</html>